<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>HAR Viewer</title>
  <link rel="stylesheet" href="https://unpkg.com/poplar-css/dist/poplar.css">
  <style>
    table {
      font-size: 12px;
    }
    tbody > tr:hover {
      background-color: #e8e8e8;
    }
    th,
    td {
      border: 1px solid #999;
      padding: 4px;
    }

    .ver__container {
      position: relative;
    }
    .ver__list {
      background-color: #fff;
      border: 1px solid #999;
      font-weight: normal;
      left: -1px;
      position: absolute;
      top: 100%;
      width: 100%;
    }
    .ver__item {
      display: block;
      padding: 5px;
    }
    .ver__checkbox {
      vertical-align: -2px;
    }
  </style>
</head>
<body>

  <div id="root"></div>

  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <script type="text/babel">
    const { useState, useEffect, useCallback } = React

    const fields = [
      { key: 'queued', title: 'Queued' },
      { key: 'started', title: 'Started' },
      { key: 'connectionStart', title: 'Connection start' },
      { key: 'requestStart', title: 'Request start' },
      { key: 'requestEnd', title: 'Request end' }
    ]
    let httpVersion = []

    function harReader (data) {
      const { log } = data

      // 在 pages 中记录的 startedDateTime
      const pagesStarted = new Date(log.pages[0].startedDateTime)
      // 第一个请求（通常是主页面）的 startedDateTime
      const firstStarted = new Date(log.entries[0].startedDateTime)
      // 谷歌浏览器导出的 har 文件中，pagesStarted 可能晚于 firstStarted
      // 所以在这里获取更早的那个时间点
      const theStarted = Math.min(pagesStarted, firstStarted)

      const entries = log.entries.map((item, index) => {
        // 请求被加入队列的时间点
        const queued = new Date(item.startedDateTime).getTime() - theStarted
        // 请求在队列中等待的时长
        const queueing = item.timings._blocked_queueing
        // 请求等待结束的时间点
        const started = queued + queueing
        // 浏览器得到指令到发出请求的时长
        const stalled = item.timings.blocked - queueing
        // 请求和服务端的连接开始的时间点
        const connectionStart = started + stalled
        // DNS 查询的时长
        const dnsLookup = item.timings.dns === -1 ? 0 : item.timings.dns
        // 建立 TCP 连接的时长
        const initialConnection = item.timings.connect === -1 ? 0 : item.timings.connect
        // SSL 认证的时长
        const ssl = item.timings.ssl === -1 ? 0 : item.timings.ssl
        // 请求开始的时间点
        const requestStart = connectionStart + dnsLookup + initialConnection + ssl
        // 请求发送（第一个字节到最后一个字节）的持续时间
        const requestSent = item.timings.send
        // 从发出请求最后一个字节到收到响应第一个字节的持续时间
        const waiting = item.timings.wait
        // 响应接收（第一个字节到最后一个字节）的持续时间
        const contentDownload = item.timings.receive
        // 请求结束的时间点
        const requestEnd = requestStart + requestSent + waiting + contentDownload

        const url = new URL(item.request.url)
        const path = url.pathname.match(/^(.*)\/([^/]*)$/)

        return {
          id: index,
          request: {
            base: (path[2] || '/') + url.search,
            dir: url.origin + path[1]
          },
          queued: queued.toFixed(3),
          queueing: queueing.toFixed(3),
          started: started.toFixed(3),
          stalled: stalled.toFixed(3),
          connectionStart: connectionStart.toFixed(3),
          dnsLookup: dnsLookup && dnsLookup.toFixed(3),
          initialConnection: initialConnection && initialConnection.toFixed(3),
          ssl: ssl && ssl.toFixed(3),
          requestStart: requestStart.toFixed(3),
          requestSent: requestSent.toFixed(3),
          waiting: waiting.toFixed(3),
          contentDownload: contentDownload.toFixed(3),
          requestEnd: requestEnd.toFixed(3),
          httpVersion: Array.from(new Set([item.request.httpVersion, item.response.httpVersion])),
          tcpConnection: item.connection,
          priority: item._priority
        }
      })

      return entries
    }

    // HTTP 版本列表
    const VersionList = ({ checkedList, onChange }) => {
      return (
        <div className="ver__list">
          {httpVersion.map((item, index) => (
            <label className="ver__item" key={index}>
              <input className="ver__checkbox" type="checkbox" value={item} checked={checkedList.indexOf(item) > -1} onChange={() => onChange(item)} /> {item}
            </label>
          ))}
        </div>
      )
    }

    const App = () => {
      const [origin, setOrigin] = useState([])
      const [entries, setEntries] = useState([])

      // 绑定拖放事件
      useEffect(() => {
        document.addEventListener('dragover', onDragOver, false)
        document.addEventListener('drop', onDrop, false)

        // 阻止浏览器直接打开文件
        function onDragOver (e) {
          e.preventDefault()
        }
        // 读取文件
        function onDrop (e) {
          e.preventDefault()

          const fileReader = new FileReader()
          const file = e.dataTransfer.files[0]
          fileReader.readAsText(file)

          fileReader.onload = function (evt) {
            const result = evt.target.result
            if (result) {
              const data = harReader(JSON.parse(result))
              updateEntries(data)
              window.sessionStorage.setItem('har', JSON.stringify(data))
            }
          }
        }

        return () => {
          document.removeEventListener('dragover', onDragOver)
          document.removeEventListener('drop', onDrop)
        }
      }, [])

      // 优先展示本地存储的资源数据
      useEffect(() => {
        const result = window.sessionStorage.getItem('har')
        if (result) {
          updateEntries(JSON.parse(result))
        }
      }, [])

      // 初始化资源数据
      const updateEntries = useCallback(data => {
        httpVersion = data.reduce((acc, cur) => {
          acc.push(...cur.httpVersion.filter(item => acc.findIndex(itm => itm === item) === -1))
          return acc
        }, [])
        setEntries(data)
        setOrigin(data)
        setVerCheckedList([...httpVersion])
        // 重置排序
        setSort([0, 0])
      }, [])

      // 将资源按各个阶段时间升/降序排列
      const [sort, setSort] = useState([0, 0])
      useEffect(() => {
        if (!origin.length) return

        const copy = [...origin]

        if (sort[1] > 0) {
          const field = fields[sort[0]].key
          copy.sort((a, b) => {
            const diff = (a[field] - b[field]) || (a.id - b.id)
            return sort[1] > 1 ? -diff : diff
          })
        }

        setEntries([...copy])
      }, [sort])
      const updateSort = useCallback((index) => {
        const newSort = [index, sort[0] === index ? (sort[1] + 1) % 3 : 1]
        setSort([...newSort])
      }, [sort])

      // http 版本筛选
      const [verVisible, setVerVisible] = useState(false)
      const [verCheckedList, setVerCheckedList] = useState([])
      const handleVerChange = useCallback((ver) => {
        const idx = verCheckedList.findIndex(item => item === ver)
        if (idx > -1) {
          verCheckedList.splice(idx, 1)
        } else {
          verCheckedList.push(ver)
        }
        setVerCheckedList([...verCheckedList])
      }, [verCheckedList])
      const showVersionList = useCallback(e => {
        e.nativeEvent.stopImmediatePropagation()
        setVerVisible(true)
      }, [])
      useEffect(() => {
        document.addEventListener('click', hideVersionList, false)

        function hideVersionList () {
          setVerVisible(false)
        }

        return () => {
          document.removeEventListener('click', hideVersionList)
        }
      }, [])

      return (
        <table>
          <thead>
            <tr>
              <th>Request</th>
              {fields.map((item, index) => (
                <th onClick={() => updateSort(index)} key={index}>
                  {item.title} {['', '↑', '↓'][sort[0] === index ? sort[1] : 0]}
                </th>
              ))}
              <th className="ver__container" onClick={showVersionList}>
                HTTP version ▽
                {verVisible && <VersionList checkedList={verCheckedList} onChange={handleVerChange} />}
              </th>
              <th>TCP connection</th>
              <th>Priority</th>
            </tr>
          </thead>
          <tbody>
            {entries.map(item => {
              return item.httpVersion.some(itm => verCheckedList.indexOf(itm) > -1) ? (
                <tr key={item.id}>
                  <td>
                    <p>{item.request.base}</p>
                    <p>{item.request.dir}</p>
                  </td>
                  <td title={`Queueing duration ${item.queueing}`}>{item.queued}</td>
                  <td title={`Stalled duration ${item.stalled}`}>{item.started}</td>
                  <td title={`DNS Lookup duration ${item.dnsLookup}, Initial connection duration ${item.initialConnection}, SSL duration ${item.ssl}`}>{item.connectionStart}</td>
                  <td title={`Request sent duration ${item.requestSent}, Waiting (TTFB) duration ${item.waiting}, Content Download duration ${item.contentDownload}`}>{item.requestStart}</td>
                  <td>{item.requestEnd}</td>
                  <td>{item.httpVersion.join(', ')}</td>
                  <td>{item.tcpConnection}</td>
                  <td>{item.priority}</td>
                </tr>
              ) : null
            })}
          </tbody>
        </table>
      )
    }

    ReactDOM.render(<App />, document.querySelector('#root'))
  </script>

</body>
</html>